﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PCRL = Pws.Clients.RestLibrary;
using PCRLS = Pws.Clients.RestLibrary.Shared;
using PCRLE = Pws.Clients.RestLibrary.ECommerce;
using PCRLC = Pws.Clients.RestLibrary.Customers;
using PCRLP = Pws.Clients.RestLibrary.Products;
using System.IO;

namespace Pws.Clients.RestLibrary.Service
{
	/*
	 * Copyright (C) PWS Distributors Ltd 2011-2014
	 * All rights reserved.
	 * 
	 */

	/// <summary>
	/// An implementation for abstracting the PWS REST service calls into a higher level concept, using IPwsObjectWrapper for performing
	/// operations on PWS REST Objects.
	/// 
	/// Client programs written using .Net should use this as their primary API for PWS REST Web Services.
	/// 
	/// Most Clients will only need the SetDefaultCredentials and GetDefaultToken methods; any further interaction is usually via the Token.
	/// 
	/// Calls to this code can catch an exception:
	/// PwsServiceConflictException,
	/// PwsServiceAuthenticationException,
	/// PwsServicePermissionException,
	/// PwsServiceValidationException,
	/// PwsServiceNullRelationshipException,
	/// PwsServiceResponseException (base Exception).
	/// 
	/// Instances and operations are thread safe.
	/// </summary>
	public interface IPwsApplication
	{
		#region Basic operation. Obtaining a Token, which most Clients use as the starting point for any interaction with the PWS REST services.
		/// <summary>
		/// Set or change the default credentials which are used as the credentials for GetDefaultToken().
		/// </summary>
		/// <param name="username">Username credential.</param>
		/// <param name="password">Password credential.</param>
		void SetDefaultCredentials(String username, String password);

		/// <summary>
		/// Most applications will use this method as the main entry point for REST operations.
		/// 
		/// Gets the default Token_V1 object, wrapped into a helper class. The Token is the fundamental object for most REST interactions as it contains
		/// a link to several operations within the context of the user login.
		/// 
		/// The Token will be obtained by using the default credentials (if they have been set), or else by SetDefaultToken being called.
		/// If the Token is due to expire soon, then an attempt will be made to renew it.
		/// </summary>
		IPwsObjectWrapperSerialisable<PCRLE.Users.Token_V1> GetDefaultToken();
		#endregion

		#region Advanced operations and primarily internal infrastructure. Most Clients will not need these.
		/// <summary>
		/// The IPwsService in use by this IPwsApplication instance.
		/// This provides the underlying mechanism for transmission using the REST interface.
		/// Generally for PWS use only; client applications will use GetDefaultToken.
		/// </summary>
		IPwsService PwsService { get; }

		/// <summary>
		/// Get the root services object, which defines the main services available. This does not require a Token to be obtained.
		/// Generally for PWS use only; client applications will use GetDefaultToken.
		/// </summary>
		/// <returns>Services_V1 object.</returns>
		PCRL.Services_V1 GetRootServices();

		/// <summary>
		/// Get a root services object by identifier.
		/// Generally for PWS use only; client applications will use GetDefaultToken.
		/// </summary>
		/// <param name="identifier">The fixed identifier name for the root service. Example is productstore.</param>
		/// <typeparam name="T">Object type to return, e.g. ProductStore_V1</typeparam>
		/// <returns>Main object for the service in question.</returns>
		IPwsObjectWrapper<T> GetRootService<T>(String identifier) where T : class, IPwsObject_V1;

		/// <summary>
		/// Post to a root services object by identifier.
		/// Generally for PWS use only; client applications will use GetDefaultToken.
		/// </summary>
		/// <param name="identifier">The fixed identifier name for the root service. Example is feedbacklist.</param>
		/// <param name="pwsObject">The PWS Object.</param>
		/// <typeparam name="T">Object type to return, e.g. Feedback_V1</typeparam>
		/// <returns>Posted object from the service in question.</returns>
		IPwsObjectWrapper<T> PostRootService<T>(String identifier, T pwsObject) where T : class, IPwsObject_V1;
		#endregion

		#region Further Token operations, e.g. elevating permissions or using secondary credentials for multiple users. Most Clients will not need these.
		/// <summary>
		/// Set a default Token explicitly by having called GetNewToken or GetRenewedToken, instead of relying upon the default credentials.
		/// This can be called at any time, regardless of whether a default token is already in place.
		/// If the Token is due to expire soon, then an attempt will be made to renew it.
		/// </summary>
		/// <param name="validToken">A Token_V1 object that has already been obtained via the GetNewToken or GetRenewedToken methods.</param>
		void SetDefaultToken(PCRLE.Users.Token_V1 validToken);

		/// <summary>
		/// Set a default Token explicitly by having called GetNewToken or GetRenewedToken, instead of relying upon the default credentials.
		/// This can be called at any time, regardless of whether a default token is already in place.
		/// If the Token is due to expire soon, then an attempt will be made to renew it.
		/// </summary>
		/// <param name="serialisedToken">A Token_V1 object that has already been obtained via the GetNewToken or GetRenewedToken methods, and has been serialised to a string using the IPwsObjectSerialisable interface.</param>
		void SetDefaultTokenFromSerialised(String serialisedToken);

		/// <summary>
		/// Set a default Token explicitly by having called GetNewToken or GetRenewedToken, instead of relying upon the default credentials.
		/// This can be called at any time, regardless of whether a default token is already in place.
		/// If the Token is due to expire soon, then an attempt will be made to renew it.
		/// The Token will be immediately retrieved from the Rest Services in order to verify it. An exception will be thrown if it is not valid.
		/// </summary>
		/// <param name="tokenString">The Token string property only, rather than fully serialised Token_V1 object.</param>
		void SetDefaultTokenFromString(String tokenString);

		/// <summary>
		/// Get a new Token, via an implicit call to GetRootServices.
		/// This does not replace the default token which is held within the IPwsApplication instance.
		/// This might be used for retrieving a token for different user credentials, in a multi-user Client environment.
		/// </summary>
		/// <param name="tokenRequest">An empty Token with the desired UserId and ExpiryTime filled in.</param>
		/// <param name="username">Username credential. This may differ from the requested Token UserId.</param>
		/// <param name="password">Password credential.</param>
		/// <returns>A new Token_V1 object.</returns>
		PCRLE.Users.Token_V1 GetNewToken(PCRLE.Users.Token_V1 tokenRequest, String username, String password);

		/// <summary>
		/// Get a renewed Token using an existing one as credentials. Makes an implicit call to GetRootServices first.
		/// This does not replace the default token which is held within the IPwsApplication instance.
		/// This might be used for retrieving a token which impersonates a different user.
		/// </summary>
		/// <param name="tokenRequest">An empty Token with the desired UserId and ExpiryTime filled in.</param>
		/// <param name="existingToken">A valid Token as authentication.</param>
		/// <returns>A new Token_V1 object.</returns>
		PCRLE.Users.Token_V1 GetRenewedToken(PCRLE.Users.Token_V1 tokenRequest, PCRLE.Users.Token_V1 existingToken);
		#endregion

		#region Performing operations on objects. Most Clients can ignore this.
		[Obsolete("Use the method in IPwsObjectWrapper instead")]
		/// <summary>
		/// Because several different users might be acting upon objects at the same time, sometimes update operations
		/// (put/post/delete) may throw an exception due to another user having made changes more recently.
		/// In order to ensure that the user is transacting the most recent version of any given object, call this method
		/// which will re-retrieve it using the "self" link within its Links array.
		/// NB: The client application is responsible for performing any applicable merge operation between the old and
		/// new version.
		/// If the object has no "self" link, then no action is taken and the supplied pwsObject is returned back again.
		/// The default Token is used as the credentials for this operation, unless it is null.
		/// </summary>
		/// <typeparam name="T">The PWS Object type being re-retrieved.</typeparam>
		/// <param name="pwsObject">The PWS Object.</param>
		/// <returns>The PWS Object representing its most recent state.</returns>
		T Refresh<T>(T pwsObject) where T : class, IPwsObject_V1;
		#endregion

		#region Wrapping objects into a more convenient form. Most Clients can ignore this.
		/// <summary>
		/// Creates a wrapper around a PWS Object, which becomes a simple starting point for REST operations.
		/// The wrapper can be used as a more convenient way to follow links to contained objects, and also as a more convenient way to Refresh the object.
		/// The default Token is used as the credentials for any operations used on the IPwsObjectWrapper, unless it is null.
		/// </summary>
		/// <typeparam name="T">Object type which contains the link to follow, e.g. Customer_V1</typeparam>
		/// <param name="pwsObject">The PWS Object to wrap.</param>
		/// <returns>IPwsObjectWrapper instance against the type of PWS Object.</returns>
		IPwsObjectWrapper<T> Wrap<T>(T pwsObject) where T : class, IPwsObject_V1;

		/// <summary>
		/// Creates a wrapper around a PWS Object, which becomes a simple starting point for REST operations.
		/// The wrapper can be used as a more convenient way to follow links to contained objects, and also as a more convenient way to Refresh the object.
		/// The default Token is used as the credentials for any operations used on the IPwsObjectWrapper, unless it is null.
		/// </summary>
		/// <typeparam name="T">Object type which contains the link to follow, e.g. Customer_V1</typeparam>
		/// <param name="pwsObject">The PWS Object to wrap.</param>
		/// <returns>IPwsObjectWrapper instance against the type of PWS Object.</returns>
		IPwsObjectWrapperSerialisable<T> WrapSerialisable<T>(T pwsObject) where T : class, IPwsObjectSerialisable_V1<T>;
		#endregion
	}

	/// <summary>
	/// Can be used as a more convenient way to follow links to contained objects; to update objects; and to Refresh the object.
	/// This is a wrapper around methods in IPwsService and/or IPwsApplication.
	/// </summary>
	/// <typeparam name="T">Object type which is wrapped, e.g. Customer_V1</typeparam>
	public interface IPwsObjectWrapper<T> where T : class, IPwsObject_V1
	{
		/// <summary>
		/// Follow a link to another object.
		/// </summary>
		/// <param name="func">Return the URI link object from this object.</param>
		/// <typeparam name="U">Object type to return, e.g. Order_V1</typeparam>
		/// <returns>Next object.</returns>
		IPwsObjectWrapper<U> Follow<U>(Func<T, Link_V1> func) where U : class, IPwsObject_V1;

		/// <summary>
		/// Get another object from a URI link within this object, specifically as a binary file. E.g. use this when retrieving a PDF document.
		/// </summary>
		/// <param name="func">Return the URI link object from this object.</param>
		/// <returns>A newly retrieved file within the standard Windows temporary directory,</returns>
		FileInfo GetBinaryFile(Func<T, Link_V1> func);

		/// <summary>
		/// Get a list of objects from a URI link within this object. This will obtain all pages of results, by repeatedly calling
		/// to get each page in turn. Please use FollowListPage if supporting pagination, or if the results list is very long,
		/// because for some large objects this method will take a long time to return.
		/// </summary>
		/// <param name="func">Return the URI link object from this object.</param>
		/// <param name="query">OData style of filter, although not always supported fully. Omit the ?$ etc. Supply query types comma separated. E.g. "filter=ProductId eq '3203'", "orderBy=OrderDate desc", "top=10", "skip=10"</param>
		/// <typeparam name="U">Object type to return, e.g. Order_V1</typeparam>
		/// <returns>List of objects of type U from service.</returns>
		IList<IPwsObjectWrapper<U>> FollowList<U>(Func<T, Link_V1> func, params String[] query) where U : class, IPwsObject_V1;

		/// <summary>
		/// Get a list of objects from a URI link within this object. This will obtain all pages of results, by repeatedly calling
		/// to get each page in turn. Please use FollowListPage if supporting pagination, or if the results list is very long,
		/// because for some large objects this method will take a long time to return.
		/// </summary>
		/// <param name="func">Return the URI link object from this object.</param>
		/// <typeparam name="U">Object type to return, e.g. Order_V1</typeparam>
		/// <returns>List of objects of type U from service.</returns>
		IList<IPwsObjectWrapper<U>> FollowList<U>(Func<T, Link_V1> func) where U : class, IPwsObject_V1;

		/// <summary>
		/// Get a list of objects from a URI link within this object, along with contained IListPage pointing to the previous and next page of results (if applicable).
		/// </summary>
		/// <param name="func">Return the URI link object from this object.</param>
		/// <param name="query">OData style of filter, although not always supported fully. Omit the ?$ etc. Supply query types comma separated. E.g. "filter=ProductId eq '3203'", "orderBy=OrderDate desc", "top=10", "skip=10"</param>
		/// <typeparam name="U">Object type to return, e.g. Order_V1</typeparam>
		/// <returns>Page containing a list of objects of type U from service.</returns>
		IListPage<U> FollowListPage<U>(Func<T, Link_V1> func, params String[] query) where U : class, IPwsObject_V1;

		/// <summary>
		/// Get a list of objects from a URI link within this object, along with contained IListPage pointing to the previous and next page of results (if applicable).
		/// </summary>
		/// <param name="func">Return the URI link object from this object.</param>
		/// <typeparam name="U">Object type to return, e.g. Order_V1</typeparam>
		/// <returns>Page containing a list of objects of type U from service.</returns>
		IListPage<U> FollowListPage<U>(Func<T, Link_V1> func) where U : class, IPwsObject_V1;

		/// <summary>
		/// Post a new object to a URI link within this object, expecting the same object type in return; and also refreshes the current object following the Post.
		/// </summary>
		/// <param name="pwsObject">The PWS Object to send.</param>
		/// <param name="func">Return the URI link object from this object.</param>
		/// <typeparam name="U">Object type to send, e.g. Order_V1</typeparam>
		/// <returns>Object of type U from service.</returns>
		IPwsObjectWrapper<U> Post<U>(Func<T, Link_V1> func, U pwsObject) where U : class, IPwsObject_V1;

		/// <summary>
		/// Post a new object to a URI link within this object, expecting the same object type in return; and also refreshes the current object following the Post.
		/// This method deliberately permits an anonymous Post if the Token is not set, and is used in special cases such as performing an anonymous search filter on a NavigationChoice object.
		/// If the Token is set correctly, then it is sent anyway; giving behaviour identical to the Post method.
		/// </summary>
		/// <param name="pwsObject">The PWS Object to send.</param>
		/// <param name="func">Return the URI link object from this object.</param>
		/// <typeparam name="U">Object type to send, e.g. Order_V1</typeparam>
		/// <returns>Object of type U from service.</returns>
		IPwsObjectWrapper<U> PostAnonymously<U>(Func<T, Link_V1> func, U pwsObject) where U : class, IPwsObject_V1;

		/// <summary>
		/// Post a binary file to a URI link within this object, expecting back a PWS Object following the post.
		/// </summary>
		/// <typeparam name="U">Object type to receive, e.g. Upload_V1</typeparam>
		/// <param name="func">Return the URI link object from this object.</param>
		/// <param name="stream">Stream object of the binary file which will be posted.</param>
		/// <param name="mediaType">Media type of the binary file which will be posted.</param>
		/// <returns>Object of type U from service.</returns>
		IPwsObjectWrapper<U> PostBinaryFile<U>(Func<T, Link_V1> func, Stream stream, String mediaType) where U : class, IPwsObject_V1;

		/// <summary>
		/// Put (update) an existing object to a URI link within this object; and also refreshes the current object following the Put.
		/// </summary>
		/// <param name="pwsObject">The PWS Object to send.</param>
		/// <param name="func">Return the URI link object from this object.</param>
		/// <typeparam name="U">Object type to send, e.g. Order_V1</typeparam>
		/// <returns>Object of type U from service.</returns>
		IPwsObjectWrapper<U> Put<U>(Func<T, Link_V1> func, U pwsObject) where U : class, IPwsObject_V1;

		/// <summary>
		/// Delete an existing object from a URI link within this object; and also refreshes the current object following the Delete.
		/// </summary>
		/// <param name="pwsObject">The PWS Object to delete.</param>
		/// <param name="func">Return the URI link object from this object.</param>
		/// <typeparam name="U">Object type to send, e.g. Order_V1</typeparam>
		void Delete<U>(Func<T, Link_V1> func, U pwsObject) where U : class, IPwsObject_V1;

		/// <summary>
		/// Delete this object.
		/// </summary>
		void DeleteSelf();

		/// <summary>
		/// Put (update) this object back to the service, by writing the contained PwsObject back to its "self" link.
		/// </summary>
		/// <returns>A refreshed instance of the same object.</returns>
		IPwsObjectWrapper<T> PutSelf();

		/// Because several different users might be acting upon objects at the same time, sometimes update operations
		/// (put/post/delete) may throw an exception due to another user having made changes more recently.
		/// In order to ensure that the user is transacting the most recent version of any given object, call this method
		/// which will re-retrieve it using the "self" link within its Links array.
		/// NB: The client application is responsible for performing any applicable merge operation between the old and
		/// new version.
		/// <returns>A refreshed instance of the same object.</returns>
		IPwsObjectWrapper<T> Refresh();

		/// <summary>
		/// The contained object.
		/// </summary>
		T PwsObject { get; }
	}

	/// <summary>
	/// When obtaining a list of Objects from IPwsObjectWrapper.FollowListPage(), this is the interface to the results.
	/// </summary>
	/// <typeparam name="U">Object type, e.g. Order_V1</typeparam>
	public interface IListPage<U> where U : class, IPwsObject_V1
	{
		/// <summary>
		/// This page of results, which has already been obtained from the Rest services;
		/// no Rest operation needs to be performed as the data is already in memory.
		/// </summary>
		IList<IPwsObjectWrapper<U>> ThisPage { get; }

		/// <summary>
		/// Get the previous page of results. This will result in a Rest operation being executed.
		/// Null if HasPreviousPage is false.
		/// </summary>
		IListPage<U> GetPreviousPage();

		/// <summary>
		/// Get the next page of results. This will result in a Rest operation being executed.
		/// Null if HasNextPage is false.
		/// </summary>
		IListPage<U> GetNextPage();

		/// <summary>
		/// True if there is a previous page of results and GetPreviousPage can be called.
		/// </summary>
		bool HasPreviousPage { get; }

		/// <summary>
		/// True if there is a next page of results and GetNextPage can be called.
		/// </summary>
		bool HasNextPage { get; }
	}

	/// <summary>
	/// Can be used as a more convenient way to follow links to contained objects; to update objects; and to Refresh the object.
	/// This is a wrapper around methods in IPwsService and/or IPwsApplication.
	/// This derived interface adds the ability to serialise the object, for convenient temporary local storage in string-based containers such as website state.
	/// </summary>
	/// <typeparam name="T">Object type which is wrapped, e.g. Token_V1</typeparam>
	public interface IPwsObjectWrapperSerialisable<T> : IPwsObjectWrapper<T> where T : class, IPwsObjectSerialisable_V1<T>
	{
		/// <summary>
		/// Serialise the object, by default to a Base64 string.
		/// </summary>
		/// <returns>Base64 encoding of the object.</returns>
		String Serialise();
	}

	/// <summary>
	/// A way of obtaining a new Token_V1 from an existing Token_V1 object,
	/// and optionally with overriding parameters such as the customer account code for impersonation.
	/// </summary>
	public interface ITokenRequest_V1
	{
		/// <summary>
		/// Execute the token renewal by calling the PWS Services via the embedded IPwsApplication instance.
		/// Exceptions will be thrown if there is a service error such as permissions or authentication.
		/// </summary>
		/// <returns>New Token_V1</returns>
		IPwsObjectWrapperSerialisable<Pws.Clients.RestLibrary.ECommerce.Users.Token_V1> GetRenewedToken();

		/// <summary>
		/// Override the customer account code.
		/// </summary>
		/// <param name="accountCode"></param>
		/// <returns>New ITokenRequest_V1</returns>
		ITokenRequest_V1 WithAccountOverride(String accountCode);

		/// <summary>
		/// Override the supplier code.
		/// </summary>
		/// <param name="supplierCode"></param>
		/// <returns>New ITokenRequest_V1</returns>
		ITokenRequest_V1 WithSupplierOverride(String supplierCode);

		/// <summary>
		/// Remove overrides on the customer account code and the supplier code, to fall back onto the UserId's default customer and supplier.
		/// </summary>
		/// <returns>New ITokenRequest_V1</returns>
		ITokenRequest_V1 WithoutOverrides();

		/// <summary>
		/// Override the expiry time.
		/// </summary>
		/// <param name="expiry">New expiry time.</param>
		/// <returns>New ITokenRequest_V1</returns>
		ITokenRequest_V1 WithExpiryTime(DateTime expiry);
	}
}
